/*
 * Computer Graphics:  Andrew Hanson, Indiana University
 * GLUT  Graphics Exercise Template 
 *

 cc -o template.linux template.c -lglut -lGLU -lGL -lm

 *
 *
 */

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <GL/glut.h>

/* Local Graphics Definitions. Also in /l/b481/include, /l/b581/include */
#include "defs.h"

/*  double y0,y1;   math.h conflict */

/***************************************************************/
/******************** MY GLOBAL VARIABLES **********************/
/***************************************************************/

int mouseButtons = 0;

int MouseModifiers = 0;

int X=0, Y=0, dX=0, dY=0;

int main_window;
int main_height, main_width;

// this is where we will store vertex coordinates
GLfloat mymat[16][2];

// this will be used in the third part
GLfloat convertedMat[16][2];

GLint selectedIndices[16];
GLint selectedIndexCount = 0;
int polygon_points_index = 0;

#define VERTEX_PROXIMITY 100
#define EDGE_PROXIMITY 10

#define BUFSIZE 512

#define POLYGON_MODE 1
#define TRANSFORM_MODE 2
#define AUTO_SELECTION_MODE 3

int my_drawing_mode= POLYGON_MODE;

GLboolean refresh_display= GL_TRUE;

int mouse_down_x, mouse_down_y;
int prev_x, prev_y;
int translate_x, translate_y;

GLfloat passive_x, passive_y;
GLfloat *current_prox_vertex;
GLboolean erase_previous= GL_TRUE;

//int selectedIndex = -1;
int selectedVertexIndex = -1;

int currentSelectedVertex = -1;

/***************************************************************/
/********************     MY FUNCTIONS    **********************/
/***************************************************************/

float getProximity(GLfloat *target, GLfloat source_x, GLfloat source_y) {
	return pow((target[0] - source_x), 2) + pow((target[1] - source_y), 2);
}

/**
 *  We have a line from (lp1x, lp1y) to (lp2x, lp2y).
 * Going to find the distance of point (target_x, target_y) to the above line.
 */
float getDistanceFromLine(int index, int target_x, int target_y) {
	GLfloat lp1x = mymat[index][0];
	GLfloat lp1y = mymat[index][1];
	GLfloat lp2x = mymat[(index+1) % polygon_points_index][0];
	GLfloat lp2y = mymat[(index+1) % polygon_points_index][1];

	GLfloat normal_vector_x = - (lp2y-lp1y);
	GLfloat normal_vector_y = lp2x-lp1x;

	GLfloat denom = sqrt(pow(normal_vector_x, 2) + pow(normal_vector_y, 2));

	float distanceH = (float) (((target_x - lp1x) * (normal_vector_x / denom))
			+ ((target_y - lp1y) * (normal_vector_y / denom))) ;

	fprintf(stderr, " DistanceH = %f\n", distanceH);

	return distanceH;

}

void drawPolygon() {
	//glMatrixMode(GL_MODELVIEW);
	int index = 0;

	// first draw the lines
	if (polygon_points_index >= 3) {
		glBegin(GL_LINE_LOOP);
		for (index = 0; index < polygon_points_index; index++) {
			glVertex2fv(mymat[index]);
		}
		glEnd();
	}

	// then draw the points on vertices
	glBegin(GL_POINTS);
	for (index = 0; index < polygon_points_index; index++) {
		glVertex2fv(mymat[index]);
	}
	glEnd();
}

void drawPolygonMode() {
	// draw a point on the mouse pointer
	glColor3f(1.0, 0.0, 1.0);
	glPointSize(5.0);
	glBegin(GL_POINTS);
	glVertex2f(X, Y);
	glEnd();

	// draw polygon
	drawPolygon();

	// if in the passive mode, check proximity
	if (X == passive_x) {
		glEnable(GL_COLOR_LOGIC_OP);
		glLogicOp(GL_XOR);
		glPointSize(10.0);

		int index = 0;

		// first check whether we have a vertex closeby
		GLboolean closeVertexFound= GL_FALSE;

		for (index = 0; index < polygon_points_index; index++) {
			if (getProximity(mymat[index], passive_x, passive_y)
					< VERTEX_PROXIMITY) {
				// redraw the current point
				closeVertexFound = GL_TRUE;
				break;
			}
		}

		if (closeVertexFound == GL_TRUE) {
			glBegin(GL_POINT);
			glColor3f(0.0, 1.0, 1.0);
			glVertex2fv(mymat[index]);
			glEnd();
		} else { // if there was no vertex close by, now let's see we can find an edge nearby
			for (index = 0; index < polygon_points_index; index++) {
				if (abs(getDistanceFromLine(index, passive_x, passive_y))
						< EDGE_PROXIMITY) {

					// if the distance is within limit, we need to find where the projection is.
					// let's calculate l (check notes), which is the projection length of the source point
					// in to our reference line.

					/**
					 *  We have a line from (lp1x, lp1y) to (lp2x, lp2y).
					 * Going to find the length of projection from (target_x, target_y) to the above line.
					 */
					GLfloat lp1x = mymat[index][0];
					GLfloat lp1y = mymat[index][1];
					GLfloat lp2x = mymat[(index+1) % polygon_points_index][0];
					GLfloat lp2y = mymat[(index+1) % polygon_points_index][1];

					GLfloat line_vector_x = lp2x-lp1x;
					GLfloat line_vector_y = lp2y-lp1y;

					GLfloat line_length = sqrt(pow(line_vector_x, 2) + pow(
							line_vector_y, 2));

					float l = (float) (((passive_x - lp1x) * (line_vector_x
							/ line_length)) + ((passive_y - lp1y)
							* (line_vector_y / line_length))) ;

					if (l > 0 && l < line_length) {
						// redraw the current line
						glBegin(GL_LINE);
						glColor3f(0.0, 1.0, 1.0);
						glVertex2fv(mymat[index]);
						glVertex2fv(mymat[(index+1) % polygon_points_index]);
						glEnd();

						// draw the nearest point to the line
						GLfloat point_on_line_x = lp1x + l * (line_vector_x
								/ line_length);
						GLfloat point_on_line_y = lp1y + l * (line_vector_y
								/ line_length);

						glPointSize(5.0);
						glBegin(GL_POINT);
						glColor3f(0.3, 0.5, 0.8);
						glVertex2f(point_on_line_x, point_on_line_y);
						glEnd();

						break;
					}
				}
			}
		}

		glDisable(GL_COLOR_LOGIC_OP);
	}
}

void rotateObject() {

	GLfloat center_of_mass_x;
	GLfloat center_of_mass_y;

	int index;
	for (index = 0; index < polygon_points_index; index++) {
		center_of_mass_x += mymat[index][0];
		center_of_mass_y += mymat[index][1];
	}

	center_of_mass_x = center_of_mass_x / (polygon_points_index + 1);
	center_of_mass_y = center_of_mass_y / (polygon_points_index + 1);

	// get the object back to initial place
	glTranslatef(center_of_mass_x, center_of_mass_y, 0.0);

	float scale_factor = 180.0 / (main_width);

	// scale it
	glRotatef((X - mouse_down_x) * scale_factor, 0.0, 0.0, 1.0);

	fprintf(stderr, "Scale X = %f, scale Y = %f\n", (X - mouse_down_x) * scale_factor, (Y - mouse_down_y) * scale_factor);

	// get the object to origin
	glTranslatef(-center_of_mass_x, -center_of_mass_y, 0.0);

}

void scaleObject() {

	GLfloat center_of_mass_x;
	GLfloat center_of_mass_y;

	int index;
	for (index = 0; index < polygon_points_index; index++) {
		center_of_mass_x += mymat[index][0];
		center_of_mass_y += mymat[index][1];
	}

	center_of_mass_x = center_of_mass_x / (polygon_points_index + 1);
	center_of_mass_y = center_of_mass_y / (polygon_points_index + 1);

	// get the object back to initial place
	glTranslatef(center_of_mass_x, center_of_mass_y, 0.0);

	//float scale_factor = 2.5 / 400;
	float x_scale;
	float y_scale;

	// are we moving towards center?
	GLboolean movingTowardsCenter = (pow((center_of_mass_x - X), 2) + pow(
			(center_of_mass_y - Y), 2) ) < (pow((center_of_mass_x - prev_x), 2)
			+ pow((center_of_mass_y - prev_y), 2) );

	if (movingTowardsCenter == GL_TRUE) {
		x_scale = (float) abs(center_of_mass_x - X)/abs(center_of_mass_x
				- mouse_down_x);
		y_scale = (float) abs(center_of_mass_y - Y)/abs(center_of_mass_y
				- mouse_down_y);
		prev_y = Y;
		prev_x = X;

	} else {
		x_scale = 1.0 + (float) abs(center_of_mass_x - X)/main_width;
		y_scale = 1.0 + (float) abs(center_of_mass_y - Y)/main_height;
	}

	glScalef(x_scale == 0 ? 1 : x_scale, y_scale == 0 ? 1 : y_scale, 1.0);

	fprintf(stderr, "Scale X = %f, scale Y = %f Diff1 = %f Diff2 = %f\n", x_scale, y_scale, (center_of_mass_x - X), (center_of_mass_x - mouse_down_x));
	//	fprintf(stderr, "Scale X = %f, scale Y = %f\n", abs(X - mouse_down_x) * scale_factor, abs(Y - mouse_down_y) * scale_factor);

	// get the object to origin
	glTranslatef(-center_of_mass_x, -center_of_mass_y, 0.0);

}

void transformObject() {
	glMatrixMode(GL_MODELVIEW);
	float mat[16];
	glGetFloatv(GL_MODELVIEW_MATRIX,mat);
	glLoadIdentity();
	if (refresh_display == GL_FALSE) {

		switch (mouseButtons) {
		case 0x1: /* Left Mouse Button   */
			glTranslatef((float)(X-prev_x), (float)(Y-prev_y), 0.0);
			break;
		case 0x2: /* Middle Mouse Button */
			scaleObject();
			break;
		case 0x4: /* Right Mouse Button  */
			rotateObject();
			break;
		}
		glMultMatrixf(mat);
		prev_x = X;
		prev_y = Y;
	} else {
		refresh_display = GL_FALSE;
	}

	drawPolygon();
}

void drawPolygonWithLabels(GLenum mode) {

	glMatrixMode(GL_MODELVIEW);

	float mat[16];
	glGetFloatv(GL_MODELVIEW_MATRIX,mat);
	glLoadIdentity();
	glMultMatrixf(mat);

	// draw lines
	int index = 0;

	printf("Selected Index Count = %d\n", selectedIndexCount);
	printf("Selected vertex index = %d\n", selectedVertexIndex);
	if (selectedVertexIndex > -1 && mouseButtons != 0x0) {
		convertedMat[selectedVertexIndex][0] = X;
		convertedMat[selectedVertexIndex][1] = Y;
	} 

	glEnable(GL_COLOR_LOGIC_OP);
	glLogicOp(GL_XOR);
	glPointSize(10.0);
	// now draw the selected index
	for (index = 0; index < selectedIndexCount; index++) {
		GLint selectedIndex = selectedIndices[index];
		if (selectedIndex> 0) {
			if (selectedIndex >= 100) {

				// then this is a vertex
				glBegin(GL_POINT);
				glColor3f(1.0, 0.6, 0.6);
				glVertex2fv(convertedMat[selectedIndex - 100]);
				glEnd();

			} else {
				glBegin(GL_LINE);
				glColor3f(1.0, 1.0, 0.6);
				glVertex2fv(convertedMat[selectedIndex - 1]);
				glVertex2fv(convertedMat[(selectedIndex) % polygon_points_index]);
				glEnd();
			}
		}
	}

	if (polygon_points_index >= 3) {

		for (index = 0; index < polygon_points_index; index++) {

			if (mode == GL_SELECT) {
				glLoadName(index+1);
				printf("Loading Name %d\n", index+1);
			}
			glBegin(GL_LINE);
			glColor3f(0.0, 1.0, 1.0);
			glVertex2fv(convertedMat[index]);
			glVertex2fv(convertedMat[(index + 1) % polygon_points_index]);
			glEnd();
		}
	}

	// draw points on vertices
	for (index = 0; index < polygon_points_index; index++) {

		if (mode == GL_SELECT) {
			glLoadName(index + 100);
			printf("Loading Name %d\n", index+100);
		}
		glBegin(GL_POINT);
		glColor3f(0.0, 0.0, 1.0);
		glVertex2fv(convertedMat[index]);
		glEnd();

	}

	selectedIndexCount = 0;
	glDisable(GL_COLOR_LOGIC_OP);

}

void processHits(GLint hits, GLuint buffer[]) {
	unsigned int i, j;
	GLuint names, *ptr;

	printf("hits = %d\n", hits);
	ptr = (GLuint *) buffer;

	for (i = 0; i < hits; i++) { /*  for each hit  */
		names = *ptr;
		/*  NOTE: the Redbook is wrong - this should give 0.5
		 NOT  0.999...   Use 0xffff... not 0x7fff..  */
		//printf(" number of names for this hit = %d\n", names);
		ptr++;
		//printf("  z1 is %g;", (float) *ptr/0xffffffff);
		ptr++;
		//printf(" z2 is %g\n", (float) *ptr/0xffffffff);
		ptr++;
		printf("   names are ");

		for (j = 0; j < names; j++) { /*  for each name */
			printf("%d ", *ptr);
			selectedIndices[selectedIndexCount++] = *ptr;
			if (*ptr >= 100) {
				selectedVertexIndex = (*ptr) - 100;
			}
		}

		ptr++;
	}

	printf("\n");

}

void selectObjects(int x, int y) {
	GLuint selectBuf[BUFSIZE];
	GLint hits;
	GLint viewport[4];

	X = x;
	Y = main_height - y - 1;

	passive_x = X;
	passive_y = Y;

	glGetIntegerv(GL_VIEWPORT, viewport);

	glSelectBuffer(BUFSIZE, selectBuf);
	(void) glRenderMode(GL_SELECT);

	glInitNames();
	glPushName(0);

	glMatrixMode(GL_PROJECTION);
	glPushMatrix();
	glLoadIdentity();
	/*  create 5x5 pixel picking region near cursor location	*/
	gluPickMatrix((GLdouble) x, (GLdouble) (viewport[3] - y), 10.0, 10.0,
			viewport);
	gluOrtho2D(0.0, main_width, 0.0, main_height);
	drawPolygonWithLabels(GL_SELECT);

	glMatrixMode(GL_PROJECTION);
	glPopMatrix();
	glFlush();

	hits = glRenderMode(GL_RENDER);
	processHits(hits, selectBuf);
	glutPostRedisplay();
}

void drawAutoSelectionPolygon() {

	// draw polygon here with labels
	drawPolygonWithLabels(GL_RENDER);

	//selectObjects();
	glFlush();

}

/***************************************************************/
/*********************** MOUSE CALLBACKS ***********************/
/***************************************************************/

/* Mouse Button  UP/DOWN  or DOWN/UP transition only */
void mouse(int button, int state, int x, int y) {
	X = x;
	Y = main_height - y - 1; /* Invert to right-handed system */
	button = (1 << button);

	MouseModifiers = glutGetModifiers();
	/* bitmask with GLUT_ACTIVE_SHIFT, GLUT_ACTIVE_CTRL, GLUT_ACTIVE_ALT */

	if (state == GLUT_DOWN) {
		mouseButtons |= button;
		mouse_down_x = X;
		mouse_down_y = Y;

		// if we are in POLYGON_MODE, then first need to find out whether we are near an existing point.
		if (my_drawing_mode == POLYGON_MODE) {
			int index;
			GLboolean vertexFound= GL_FALSE;
			for (index = 0; index < polygon_points_index; index++) {
				if (getProximity(mymat[index], mouse_down_x, mouse_down_y)
						< VERTEX_PROXIMITY) {
					// redraw the current point
					currentSelectedVertex = index;
					vertexFound = GL_TRUE;
					break;
				}
			}

			if (vertexFound == GL_FALSE) {
				currentSelectedVertex = polygon_points_index++;
			}

			mymat[currentSelectedVertex][0] = X;
			mymat[currentSelectedVertex][1] = Y;
		}

		prev_x = X;
		prev_y = Y;
	} else {
		mouseButtons &= (~(button));
		//		if (polygon_points_index < 14 && my_drawing_mode == POLYGON_MODE) {
		//			mymat[polygon_points_index][0] = X;
		//			mymat[polygon_points_index++][1] = Y;
		//		} else 
		if (my_drawing_mode == TRANSFORM_MODE) {
			prev_x = X;
			prev_y = Y;
		}
	}

	//	fprintf(stderr,"Comment this out: mouse buttons = [%d], modifiers = [%d] \n\
//          state = %s, mouse at (%d,%d)\n",
	//	mouseButtons, MouseModifiers,
	//	state == GLUT_UP? "Just Up":"Just Down",x,y);

	glutPostRedisplay();
}

/* Mouse motion WHILE BUTTON DOWN  */
/*  COMPLAINS  if you call glutGetModifiers()  here!! */
void motion(int x, int y) {
	//	fprintf(stderr,"Comment this out: mouse at (%d,%d)\n\
//          mouse buttons = [%d], modifiers = [%d]\n",
	//	x,y, mouseButtons, MouseModifiers);
	X = x;
	Y = main_height - y - 1; /* Invert to right-handed system */

	switch (mouseButtons) {
	case 0x1: /* Left Mouse Button   */
		if (my_drawing_mode == TRANSFORM_MODE) {
			translate_x = X - mouse_down_x;
			translate_y = Y - mouse_down_y;
			//prev_x = X;
			//prev_y = Y;
		} else if (my_drawing_mode == POLYGON_MODE) {
			mymat[currentSelectedVertex][0] = X;
			mymat[currentSelectedVertex][1] = Y;
		}
		break;
	case 0x2: /* Middle Mouse Button */
		break;
	case 0x4: /* Right Mouse Button  */
		break;
	}

	glutPostRedisplay();

}

/* Mouse motion WHILE ALL BUTTONS UP  */
/*  COMPLAINS  if you call glutGetModifiers()  here!! */
void passive(int x, int y) {
	X = x;
	Y = main_height - y - 1;

	passive_x = X;
	passive_y = Y;

	//fprintf(stderr,"Comment this out: mouse at (%d,%d)\n", x,y);

	glutPostRedisplay();
}

/* Handle Keyboard */
void keyboard(unsigned char key, int x, int y) {
	/*  Option: To force all lower case */
	/* if (key >= 'A' && key <= 'Z') key = (key-'A') + 'a'; */

	switch (key) {
	case 'h':
		fprintf(stderr,"Help: goes here.\n");
		break;
	case '1':
		my_drawing_mode = POLYGON_MODE;
		glutPassiveMotionFunc(passive);
		glMatrixMode(GL_PROJECTION);
		break;
	case '2':
		my_drawing_mode = TRANSFORM_MODE;
		glutPassiveMotionFunc(passive);
		prev_x = X;
		prev_y = Y;
		break;
	case '3':
		my_drawing_mode = AUTO_SELECTION_MODE;
		glutPassiveMotionFunc(selectObjects);

		// copy the user cordinates
		int index;
		for (index = 0; index < polygon_points_index; index++) {
			convertedMat[index][0] = mymat[index][0];
			convertedMat[index][1] = mymat[index][1];
		}

		break;
	case 'R':
		refresh_display = GL_TRUE;
		prev_x = X;
		prev_y = Y;
		break;
	case 'r':
		polygon_points_index = 0;
		refresh_display = GL_TRUE;
		prev_x = X;
		prev_y = Y;
		break;
	case 'q':
	case 27: /* 27 -> ESC */
		fprintf(stderr,"Normal Exit.\n");
		exit(EXIT_SUCCESS);
		break;
	default:
		fprintf(stderr,"Unhandled key: %c [%d] \n",key,key);
	}

	glutPostRedisplay();
}

/***************************************************************/
/************************** INIT GL ****************************/
/***************************************************************/

void initGL() {
	glClearColor(0.0, 0.0, 0.0, 0.0);
}

/***************************************************************/
/************************ DRAW STUFF ***************************/
/***************************************************************/

void drawObjs() {
	glColor3f(0.9, 0.8, 0.4);

	switch (my_drawing_mode) {
	case POLYGON_MODE:
		drawPolygonMode();
		break;
	case TRANSFORM_MODE:
		transformObject();
		break;

	case AUTO_SELECTION_MODE:
		drawAutoSelectionPolygon();
		break;

	}

}

/***************************************************************/
/************************ GLUT STUFF ***************************/
/***************************************************************/

void reshape(int w, int h) {
	fprintf(stderr,"Comment this out: reshape(%d,%d) \n",w,h);
	w = Max(w,128);
	h = Max(h,128);
	main_height = h;
	main_width = w;
	glutReshapeWindow(w, h);
	glViewport(0, 0, w, h);
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();

	gluOrtho2D(0.0, (double)w-1, 0.0, (double)h-1);

	glMatrixMode(GL_MODELVIEW);
}

void visibility(int state) {
	/* May also want to check PARTIAL DAMAGE that
	 does not result in a call to the visibility callback:
	 if (glutLayerGet(GLUT_NORMAL_DAMAGED)) {
	 redraw = TRUE; }
	 */
	fprintf(stderr,"Comment this out: visibility: %s\n",
	(state==GLUT_VISIBLE)?"visible":"invisible");
	if (state == GLUT_VISIBLE) {
		glutPostRedisplay();
	}
}

void display(void) {
	//fprintf(stderr,"Comment this out: display\n");

	glClear(GL_COLOR_BUFFER_BIT);

	drawObjs();

	glutSwapBuffers();
}

/***************************************************************/
/**************************** MAIN *****************************/
/***************************************************************/

int main(int argc, char **argv) {
	/* GLUT Initialization */
	glutInit(&argc, argv);
	glutInitWindowSize(500, 500);
	glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB);

	/* Create a new window and give a title to it */
	main_window = glutCreateWindow(" :: Eran Chinthaka - PS3 :: ");

	/* Initialize OpenGL stuff */
	initGL();

	/* Post Event Handlers */
	glutReshapeFunc(reshape);
	glutVisibilityFunc(visibility);
	glutDisplayFunc(display);
	glutMouseFunc(mouse);
	glutMotionFunc(motion);
	glutPassiveMotionFunc(passive);

	glutKeyboardFunc(keyboard);
	fprintf(stderr,"Press ESC or 'q' to Exit.\n");

	glutIdleFunc(NULL);

	glutMainLoop();

	return (EXIT_SUCCESS);
}
